/*
 * 
 * Copyright 2011 by Mark Coletti, Keith Sullivan, Sean Luke, and George Mason University Mason University Licensed
 * under the Academic Free License version 3.0
 * 
 * See the file "LICENSE" for more information
 * 
 * $Id$
 */
package sim.portrayal.geo;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.util.HashMap;

import sim.field.continuous.Continuous2D;
import sim.field.geo.GeomVectorField;
import sim.portrayal.*;
import sim.util.Bag;
import sim.util.Double2D;
import sim.util.geo.MasonGeometry;

/**
 * Portrayal for MasonGeometry objects. The portrayal handles drawing and hit-testing (for inspectors).
 * 
 * 
 * <p>
 * GeomVectorFieldPortrayal overrides getPortrayalForObject to do a different thing than normal FieldPortrayals.
 * Specifically:
 * 
 * <p>
 * <ol>
 * <li>The object passed in is expected to be a MasonGeometry. From this we extract USER, the MASON user data of the
 * geometry, and GEOMETRY, the JTS Geometry object.
 * <li>If there is a portrayalForAll, return it.
 * <li>If user exists and is a Portrayal, return user as the Portrayal.
 * <li>If a portrayal is registered for user, return it.
 * <li>If a portrayal is registered for geometry, return it.
 * <li>If a portrayal is registered for user's class, return it.
 * <li>If a portrayal is registered for the geometry's class, return it.
 * <li>If there is a portrayalForRemainder, return it.
 * <li>Else return the getDefaultPortrayal
 * </ol>
 * 
 * <p>
 * Note that nowhere do we return portrayals for null objects: there is no PortrayalForNull and no DefaultNullPortrayal.
 * Indeed, the method setPortrayalForNull will throw an error -- you are not permitted to call it.
 */
public class GeomVectorFieldPortrayal extends FieldPortrayal2D
{

	/** Throws an exception. Do not call this method. */
	@Override
	public void setPortrayalForNull(Portrayal portrayal)
	{
		// this bad boy throws an exception
		throw new RuntimeException("setPortrayalForNull(Portrayal) may NOT be called on a GeomVectorFieldPortrayal");
	}

	/**
	 * Returns the appropriate Portrayal. See the class header for more information on the implementation of this
	 * method.
	 */
	@Override
	public Portrayal getPortrayalForObject(Object obj)
	{
		// return the portrayal-for-all if any
		if (portrayalForAll != null) { return portrayalForAll; }

		MasonGeometry mg = (MasonGeometry) obj;
		Geometry geometry = mg.getGeometry();
		Object user = mg.getUserData();

		Portrayal tmp;

		// we don't check for null values of obj, so this is simpler than the
		// one in FieldPortrayal

		if (user != null && user instanceof Portrayal) { return (Portrayal) user; }
		if (portrayalForNonNull != null) { return portrayalForNonNull; }
		if ((portrayals != null /* && !portrayals.isEmpty() */) && // a little
				// efficiency
				// -- avoid
				// making
				// weak keys
				// etc.
				((tmp = ((Portrayal) (portrayals.get(user)))) != null)) { return tmp; }
		if ((portrayals != null /* && !portrayals.isEmpty() */) && // a little
				// efficiency
				// -- avoid
				// making
				// weak keys
				// etc.
				((tmp = ((Portrayal) (portrayals.get(geometry)))) != null)) { return tmp; }
		if (user != null && (classPortrayals != null /*
														* &&
														* !classPortrayals.isEmpty
														* ()
														*/) && // a little
				// efficiency --
				// avoid making weak
				// keys etc.
				((tmp = ((Portrayal) (classPortrayals.get(user.getClass())))) != null)) { return tmp; }
		if (geometry != null && (classPortrayals != null /*
															* &&
															* !classPortrayals.isEmpty
															* ()
															*/) && // a little
				// efficiency --
				// avoid making
				// weak keys
				// etc.
				((tmp = ((Portrayal) (classPortrayals.get(geometry.getClass())))) != null)) { return tmp; }
		if (portrayalForRemainder != null) { return portrayalForRemainder; }

		return getDefaultPortrayal();
	}

	private static final long serialVersionUID = 8409421628913847667L;

	/** The underlying portrayal */
	GeomPortrayal defaultPortrayal = new GeomPortrayal();

	/** Default constructor */
	public GeomVectorFieldPortrayal()
	{
		super();
		setImmutableField(false);
	}

	/** Constructor which sets the field's immutable flag */
	public GeomVectorFieldPortrayal(boolean immutableField)
	{
		super();
		setImmutableField(immutableField);
	}

	/** Return the underlying portrayal */
	@Override
	public Portrayal getDefaultPortrayal()
	{
		return defaultPortrayal;
	}

	/** Caches immutable fields. */
	BufferedImage buffer = null;

	RenderingHints hints = null;

	/** Handles hit-testing and drawing of the underlying geometry objects. */
	@Override
	protected void hitOrDraw(Graphics2D graphics, DrawInfo2D info, Bag putInHere)
	{
		if (field == null) { return; }

		// If we're drawing (and not inspecting), re-fresh the buffer if the
		// associated field is immutable.
		if (graphics != null && immutableField && !info.precise)
		{

			GeomVectorField geomField = (GeomVectorField) field;
			double x = info.clip.x;
			double y = info.clip.y;
			boolean dirty = false;

			// make a new buffer? or did the user change the zoom? Or change the
			// rendering hints?
			if (buffer == null || buffer.getWidth() != info.clip.width || buffer.getHeight() != info.clip.height
					|| hints == null || !hints.equals(graphics.getRenderingHints()))
			{
				hints = graphics.getRenderingHints();
				buffer = new BufferedImage((int) info.clip.width, (int) info.clip.height, BufferedImage.TYPE_INT_ARGB);
				dirty = true;
			}

			// handles the case for scrolling
			if (geomField.drawX != x || geomField.drawY != y)
			{
				dirty = true;
			}

			// save the origin of the drawn region for later
			geomField.drawX = x;
			geomField.drawY = y;

			// re-draw into the buffer
			if (dirty)
			{
				clearBufferedImage(buffer);
				Graphics2D newGraphics = (Graphics2D) buffer.getGraphics();
				newGraphics.setRenderingHints(hints);
				hitOrDraw2(newGraphics, new DrawInfo2D(info, -x, -y), putInHere);
				newGraphics.dispose();
			}

			// draw buffer on screen
			graphics.drawImage(buffer, (int) x, (int) y, null);
		}
		else if (graphics == null) // we're just hitting
		{
			hitOrDraw2(graphics, info, putInHere);
		}
		else
		// might as well clear the buffer -- likely we're doing precise drawing
		{
			// do regular MASON-style drawing
			buffer = null;
			hitOrDraw2(graphics, info, putInHere);
		}
	}

	/** Clears the BufferedImage by setting all the pixels to RGB(0,0,0,0) */
	void clearBufferedImage(BufferedImage image)
	{
		int len = image.getHeight() * image.getWidth();
		WritableRaster raster = image.getRaster();
		int[] data = new int[len];
		for (int i = 0; i < len; i++)
		{
			data[i] = 0;
		}
		raster.setDataElements(0, 0, image.getWidth(), image.getHeight(), data);
	}

	/**
	 * Helper function which performs the actual hit-testing and drawing for both immutable fields and non-immutable
	 * fields.
	 * 
	 * <p>
	 * The objects in the field can either use GeomPortrayal or any SimplePortrayal2D for drawing.
	 * 
	 */
	void hitOrDraw2(Graphics2D graphics, DrawInfo2D info, Bag putInHere)
	{
		GeomVectorField geomField = (GeomVectorField) field;
		if (geomField == null) { return; }

		boolean objectSelected = !selectedWrappers.isEmpty();

		geomField.updateTransform(info);

        Bag geometries;

        geometries = geomField.queryField(geomField.clipEnvelope);

        if (geometries == null || geometries.isEmpty())
        {
            // FIXME This is a hack to correct for situation where when
            // doing hit, not drawing, the incorrect geometries are returned.
            geometries = geomField.getGeometries();

            // Sometimes there really *isn't* anything to render.
            if (geometries.isEmpty())
            {
                return;
            }
        }
//        else
//        {
//            System.out.println("clipped: " + geometries.size());
//        }

        GeomInfo2D gInfo = new GeomInfo2D(info, geomField.worldToScreen);

		final double xScale = info.draw.width / geomField.getFieldWidth();
		final double yScale = info.draw.height / geomField.getFieldHeight();
		GeomInfo2D newinfo = new GeomInfo2D(new DrawInfo2D(info.gui, info.fieldPortrayal, new Rectangle2D.Double(0, 0,
				xScale, yScale), info.clip), geomField.worldToScreen);
		newinfo.fieldPortrayal = this;
		
		// use this for determining which objects we should be concerned with 
		GeometryFactory geomFactory = ((MasonGeometry)geometries.objs[0]).getGeometry().getFactory();
		Geometry clipGeometry = geomFactory.toGeometry(geomField.clipEnvelope);
		
		for (int i = 0; i < geometries.size(); i++)
		{
			MasonGeometry gm = (MasonGeometry) geometries.objs[i];

            // FIXME: *Why* the hell would this happen?
            if (gm == null) { continue; }

			Geometry geom = gm.getGeometry();
			
			if (clipGeometry.intersects(geom.getEnvelope()))
			{
				Portrayal p = getPortrayalForObject(gm);

				if (!(p instanceof SimplePortrayal2D)) { throw new RuntimeException("Unexpected Portrayal " + p
						+ " for object " + gm + " -- expected a SimplePortrayal2D or a GeomPortrayal"); }

				SimplePortrayal2D portrayal = (SimplePortrayal2D) p;

				if (graphics == null)
				{
					if (portrayal.hitObject(gm, info))
					{
                        // XXX getGeometryLocation merely returns the centroid of
                        // the MasonGeometry object once it finds it in the GeomVectorField;
                        // however, we *just got it* from that same field, so why
                        // do we need to find it again?  Just directly get the
                        // centroid of the object and be done with it.
//						putInHere.add(new LocationWrapper(gm, geomField.getGeometryLocation(gm), this));
						putInHere.add(new LocationWrapper(gm, gm.getGeometry().getCentroid(), this));
					}
				}
				else
				{
					if (portrayal instanceof GeomPortrayal)
					{
						portrayal.draw(gm, graphics, gInfo);
					}
					else
					{ // have a SimplePortrayal2D,
						Point pt = gm.geometry.getCentroid();
						pt.apply(geomField.jtsTransform);
						pt.geometryChanged();

						newinfo.selected = (objectSelected && selectedWrappers.get(gm) != null);
						newinfo.draw.x = pt.getX();
						newinfo.draw.y = pt.getY();
						portrayal.draw(gm, graphics, newinfo);
					}
				}
			}
		}
	}

	/** Sets the underlying field, after ensuring its a GeomVectorField. */
	@Override
	public void setField(Object field)
	{
		if (field instanceof GeomVectorField)
		{
			super.setField(field);
		} // sets dirty field already
		else
		{
			throw new RuntimeException("Invalid field for GeomFieldPortrayal: " + field);
		}
	}

	HashMap<Object, LocationWrapper> selectedWrappers = new HashMap<Object, LocationWrapper>();

	@Override
	public boolean setSelected(LocationWrapper wrapper, boolean selected)
	{
		if (wrapper == null) { return true; }
		if (wrapper.getFieldPortrayal() != this) { return true; }

		Object obj = wrapper.getObject();
		boolean b = getPortrayalForObject(obj).setSelected(wrapper, selected);
		if (selected)
		{
			if (b == false) { return false; }
			selectedWrappers.put(obj, wrapper);
		}
		else
		{
			selectedWrappers.remove(obj);
		}
		return true;
	}

}
